# coding=utf-8
"""
Author: Mr.夏末
@Function: Web自动化关键字封装
"""
import re, os, json, yaml, time, threading, requests, jsonpath, pymysql, selenium, appium

from selenium.webdriver import ActionChains
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.support.select import Select

from common.Logger import logger, path
from keywords.desc import relations
from keywords.verify import Verify

from appium.webdriver.common.appiumby import AppiumBy

from selenium.webdriver.chrome.service import Service as ChromeService
from webdriver_manager.chrome import ChromeDriverManager
from selenium.webdriver.firefox.service import Service as FirefoxService
from webdriver_manager.firefox import GeckoDriverManager
from selenium.webdriver.edge.service import Service as EdgeService
from webdriver_manager.microsoft import EdgeChromiumDriverManager

class Web:
    """web自动化关键字类"""

    def __init__(self):
        self.driver: selenium.webdriver.Chrome = None
        # 使用字典保存关联的数据
        self.relation_dict = {}

    def openbrowser(self, br=''):
        """
        打开浏览器
        :param br: 默认是谷歌浏览器，支持IE、Firefox、Edge
        :return:
        """
        # 自动读取浏览器版本并更新安装驱动
        if br == 'ff':
            self.driver = selenium.webdriver.Firefox(service=FirefoxService(GeckoDriverManager().install()))
        elif br == 'edge':
            self.driver = selenium.webdriver.Edge(service=EdgeService(EdgeChromiumDriverManager().install()))
        else:
            # 使用options，去掉自动化软件控制的标识
            option = Options()
            option.add_experimental_option('excludeSwitches', ['enable-automation'])
            option.add_argument('--disable-blink-features=AutomationControlled')
            ##关掉密码弹窗
            prefs = {}
            prefs['credentials_enable_service'] = False
            prefs['profile.password_manager_enabled'] = False
            option.add_experimental_option('prefs', prefs)
            self.driver = selenium.webdriver.Chrome(options=option, service=ChromeService(ChromeDriverManager().install()))
        # 默认配置隐式等待
        self.driver.implicitly_wait(10)
        self.driver.maximize_window()

    def get_url(self, url=''):
        """
        打开网站
        :param url: 网站地址
        :return:
        """
        if url.startswith('http'):
            self.driver.get(url)
        else:
            self.driver.get('http://' + url)

    def __find_ele(self, locator=''):
        if locator.startswith('/') or locator.startswith('('):
            ele = self.driver.find_element('xpath', locator)
        elif locator.startswith('#'):
            ele = self.driver.find_element('css selector', locator)
        else:
            ele = self.driver.find_element('id', locator)
        # 给操作的元素高亮显示
        if ele:
            self.driver.execute_script('arguments[0].style.background = "#00ff50"', ele)
        return ele

    @relations
    def input(self, locator='', value=''):
        """输入
        :param locator: 定位器 xpath、CSS、ID、ClassName
        :param value: 输入的值
        """
        ele = self.__find_ele(locator)
        if value.endswith('.jpg') or value.endswith('.png'):
            value = path + 'lib\\' + value
        ele.send_keys(value)

    def clear(self, locator):
        """清空输入框"""
        ele = self.__find_ele(locator)
        ele.clear()

    def click(self, locator):
        """点击"""
        ele = self.__find_ele(locator)
        ele.click()

    def clicktry(self, locator):
        """点击"""
        try:
            time.sleep(2)
            ele = self.__find_ele(locator)
            ele.click()
        except Exception as e:
            print('尝试点击元素%s错误' % locator)
        finally:
            time.sleep(1)

    def slide12306(self):
        # 找到滑块
        ele1 = self.__find_ele('//span[@id="nc_1_n1z"]')
        ele2 = self.__find_ele('//span[@class="nc-lang-cnt"]')
        # 使用action滑动
        action = ActionChains(self.driver)
        # 按住滑块
        action.click_and_hold(ele1)
        # 拖到最右边
        action.move_by_offset(ele2.size.get('width'), 0)
        # 松开鼠标
        action.release()
        # 让这些动作执行
        action.perform()

    def get_verify(self, locator):
        # 截取验证码图片
        vimg = self.__find_ele(locator)
        vimg.screenshot(path + 'class11\\verify.png')
        # 获取验证码
        verify = Verify('wuqingfqng', 'wuqing&fqng', '904357')
        # verify = Verify('17863087593','hzs123456','937288')
        # 调用方法，获取验证码
        ver = verify.get_verify(path + 'class11\\verify.png')
        self.relation_dict['verify'] = ver
        return ver

    def jsclick(self, locator):
        """js点击"""
        ele = self.__find_ele(locator)
        self.driver.execute_script('return arguments[0].className', ele)

    def get_text(self, locator, reg=''):
        """
        获取元素文本
        :param locator:
        :param reg:对文本进行正则处理
        """
        ele = self.__find_ele(locator)
        text = ele.text
        # 如果有正则，按照正则处理
        if reg:
            text = re.findall(reg, text)
            if text:
                text = text[0]
        # 获取到属性可以变成一个系统变量，{text}保存起来
        self.relation_dict['text'] = text
        return text

    def gettitle(self):
        """获取标题"""
        title = self.driver.title
        self.relation_dict['title'] = title
        return title

    @relations
    def saveparams(self, param_name='', value=''):
        """
        把参数保存为特定名字
        :param param_name: 参数名
        :param value: 参数值
        """
        self.relation_dict[param_name] = value

    def sleep(self, t='1'):
        try:
            t = float(t)
        except Exception as e:
            print(e.__str__())
            t = 1
        time.sleep(t)

    def select(self, locator='', value=''):
        # 找到select元素
        ele = self.__find_ele(locator)
        # 用select元素创建selenium的select对象
        select = Select(ele)
        try:
            # 整数使用value
            value = int(value)
            select.select_by_value(str(value))
        except:
            # 其他使用text
            select.select_by_visible_text(value)

    def assertno(self, value1='', value2=''):
        """
        断言不相等
        :param value1: 期望值
        :param value2: 实际值
        """
        if value1 == value2:
            print('断言失败')
            return False, f"断言失败，实际结果{value2}"
        else:
            print('断言成功')
            return True

    def switchwin(self, title=''):
        # 切换窗口
        # 获取窗口的标志id列表
        handles = self.driver.window_handles
        print(handles)
        # 按下标
        try:
            title = int(title)
            self.driver.switch_to.window(title)
        except:
            # 标题切换
            for i in handles:
                self.driver.switch_to.window(i)
                if self.driver.title.__contains__(title):
                    break

    def Iframe(self, loicator=''):
        """进入iframe"""
        ele = self.__find_ele(loicator)
        self.driver.switch_to.frame(ele)

    def outiframe(self, loicator=''):
        """退出iframe"""
        self.driver.switch_to.default_content()


class APP:

    def __init__(self):
        self.driver: appium.webdriver.Remote = None

    def runappium(self, port='4723', appium_path='C:\Program Files\Appium'):
        """
        启动appium服务
        :param appium_path: appium安装根路径
        """
        if not appium_path:
            appium_path = 'C:\Program Files\Appium'

        if not port:
            port = '4723'

        self.port = port

        def runappium(appium_path, port):
            # 打开appium服务
            os.system(r'node "%s\resources\app\node_modules\appium\build\lib\main.js" -p %s >> appium.log' % (
            appium_path, port))

        # 创建一个线程,如果函数需要参数，就用args=传递
        th = threading.Thread(target=runappium, args=(appium_path, port))

        # 启动线程
        th.start()

    def run_app(self, conf='{}'):
        """
        启动APP
        :param conf:配置的json字符串
        """
        conf = json.loads(conf)
        # 连接appium服务器（回放之前一定要启动appium服务）
        self.driver = appium.webdriver.Remote("http://127.0.0.1:%s/wd/hub" % self.port, conf)
        self.driver.implicitly_wait(30)

    def __find_ele(self, locator=''):
        """
        app元素定位
        :param locator: 支持ACCESSIBILITY_ID，ID，XPATH
        """
        if locator.startswith('(') or locator.startswith('/'):
            # xpath
            ele = self.driver.find_element(AppiumBy.XPATH, locator)
        elif locator.__contains__(":id/"):
            ele = self.driver.find_element(AppiumBy.ID, locator)
        else:
            ele = self.driver.find_element(AppiumBy.ACCESSIBILITY_ID, locator)

        return ele

    def click(self, locator=''):
        """
        app点击
        :param locator: 支持ACCESSIBILITY_ID，ID，XPATH
        """
        ele = self.__find_ele(locator)
        ele.click()

    def input(self, locator='', value=''):
        """
        app输入
        :param locator: 支持ACCESSIBILITY_ID，ID，XPATH
        :param value: 输入的文本
        """
        ele = self.__find_ele(locator)
        ele.send_keys(value)

    def presskey(self, keycode='66'):
        """
        按键
        :param keycode: 键对应的键码
        """
        try:
            keycode = int(keycode)
            self.driver.keyevent(keycode)
        except:
            logger.warning("只支持数字keycode")
            pass

    def quit(self):
        self.driver.quit()

    def sleep(self, t='1'):
        try:
            t = float(t)
        except:
            t = 1

        time.sleep(t)

    def closeappium(self):
        # 关闭appium
        os.system('taskkill /F /IM node.exe')


class Inter:
    """接口关键字类"""

    def __init__(self):
        """使用会话管理"""
        self.session = requests.session()
        # 保存结果
        self.jsonres = {}
        self.textres = ''
        # 关联字典
        self.relation_dict = {}
        # url前缀
        self.url = ''

    def seturl(self, url):
        """
        设置url前缀 主要是host信息
        :param url: 基准url
        """
        self.url = url
        return self.url

    @relations
    def get(self, url='', params=None):
        """
        get方法
        :param url: 接口地址
        :param params: 接口参数
        """
        # 处理url
        if url.startswith('http'):
            # 接口使用了完整url
            pass
        else:
            url = self.url + '/' + url

        # 处理参数
        params = self.__get_params_data(params)

        try:
            res = self.session.get(url, params=params)
        except Exception as e:
            logger.exception(e)
            self.textres = e.__str__()
            return False, self.textres
        # 对结果进行JSON解析
        try:
            self.jsonres = json.loads(res.text)
            return self.jsonres
        except:
            # 如果不能解析为JSON，就直接使用字符串
            self.jsonres = {}
            self.textres = res.text
            return self.textres

    @relations
    def post(self, url='', params=None):
        """
        post方法
        :param url: 接口地址
        :param params: 接口参数
        """
        # 处理url
        if url.startswith('http'):
            # 接口使用了完整url
            pass
        else:
            url = self.url + '/' + url

        #处理参数
        params = self.__get_params_data(params)

        try:
            res = self.session.post(url, data=params)
        except Exception as e:
            logger.exception(e)
            self.textres = e.__str__()
            return False, self.textres
        # 对结果进行JSON解析
        try:
            self.jsonres = json.loads(res.text)
            return self.jsonres
        except:
            # 如果不能解析为JSON，就直接使用字符串
            self.jsonres = {}
            self.textres = res.text
            return self.textres

    @relations
    def addheader(self, key='', value=''):
        """
        头里面添加键值对
        :param key: 键
        :param value: 值
        """
        self.session.headers[key] = value
        logger.info(self.session.headers)
        return self.session.headers

    def removeheader(self, key=''):
        """
        删除头
        :param key: 删除的键(如果是空或者空字符串，就清空头)
        :param value: 键的值
        """
        if key:
            try:
                self.session.headers.pop(key)
            except:
                pass
        else:
            self.session.headers = {}
        logger.info(self.session.headers)
        return self.session.headers

    def reauth(self):
        """重新授权"""
        if self.url:
            self.session.post(self.url + "/logout")
            res = self.session.post(self.url + "/auth")
            self.session.headers['token'] = res.json().get('token')
            logger.info(self.session.headers)
        else:
            self.session.post("http://www.testingedu.com.cn:8081/inter/HTTP/logout")
            res = self.session.post("http://www.testingedu.com.cn:8081/inter/HTTP/auth")
            self.session.headers['token'] = res.json().get('token')
            logger.info(self.session.headers)
        return self.session.headers

    @relations
    def savejson(self, json_path='', p_name=''):
        """
        从上一步请求里面保存一个键为某个参数
        :param key: 键名
        :param p_name: 参数名
        """
        if json_path:
            value = jsonpath.jsonpath(self.jsonres, json_path)
            if value:
                value = value[0]
            else:
                value = str(value)

            if p_name:
                self.relation_dict[p_name] = value
            else:
                self.relation_dict[json_path] = value
        else:
            # 如果我保存返回结果里面某个小json
            if p_name:
                for key in self.jsonres.get(p_name).keys():
                    self.relation_dict[key] = str(self.jsonres.get(key))
            else:
                for key in self.jsonres.keys():
                    self.relation_dict[key] = str(self.jsonres.get(key))
        return self.relation_dict

    @relations
    def assertequals(self, json_path='', value=''):
        """
        断言请求结果里面某个键的值是预期的value
        :param key: 断言的键
        :param value: 预期的值
        """
        actual_value = jsonpath.jsonpath(self.jsonres,json_path)
        if actual_value:
            actual_value = actual_value[0]

        if str(value) == str(actual_value):
            logger.info(f'实际结果: {actual_value},预期结果: {value}')
            logger.info("PASS")
            return True,"PASS"
        else:
            logger.info(f'实际结果: {actual_value},预期结果: {value}')
            logger.error("FAIL")
            return False,f"FAIL: {actual_value},{value}"

    @relations
    def assertcontains(self, json_path='', value=''):
        """
        断言请求结果里面某个键的值是预期的value
        :param key: 断言的键
        :param value: 预期的值
        """
        actual_value = jsonpath.jsonpath(self.jsonres, json_path)
        if actual_value:
            actual_value = actual_value[0]

        if str(value).__contains__(str(actual_value)):
            logger.info(f'实际结果: {actual_value},预期结果: {value}')
            logger.info("PASS")
            return True, "PASS"
        else:
            logger.info(f'实际结果: {actual_value},预期结果: {value}')
            logger.error("FAIL")
            return False, f"FAIL: {actual_value},{value}"

    def __get_params_data(self,params):
        """
        json/字典、url格式参数转字典
        :param params: 字符串参数
        """
        if params:
            params_dict = {}
            try:
                #先处理是不是字典
                params_dict = eval(params)
                return params_dict
            except:
                pass

            #再处理JSON字符串
            try:
                #先处理是不是JSON
                params_dict = json.loads(params)
                return params_dict
            except:
                pass

            # 切割键值对
            params = params.split('&')
            for p in params:
                i = p.find('=')
                if i > 0:
                    params_dict[p[:i]] = p[i + 1:]
                else:
                    params_dict[p] = None
            logger.info('参数'+ str(params_dict))
            return params_dict
        else:
            return None

    def exesql(self,sql=''):
        """
        执行SQL并返回查询结果
        :param sql: SQL语句
        """
        with open(file=path + "lib/conf.yml", mode='r', encoding="utf-8")as file:
            self.conf = yaml.safe_load(stream=file).get('mysql')

        self.sqlpath = path + 'lib\\' + self.conf['sqlpath']
        self.conf.pop('sqlpath')
        # 创建连接，执行语句的时候是在这个连接上执行
        connect = pymysql.connect(**self.conf)
        cursor = connect.cursor()
        # 执行单条语句的示例
        cursor.execute(sql)
        # 获取查询结果
        res = cursor.fetchall()
        # 关闭游标和连接
        cursor.close()
        connect.close()
        return res
